{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "055a5549",
   "metadata": {},
   "source": [
    "首先确定输入数据的特征\n",
    "\n",
    "假设我们有一个输入图像，它的大小是28x28x3，其中3表示通道数，即图像有3个颜色通道（红色、绿色和蓝色）。\n",
    "\n",
    "然后确定卷积层的卷积核特征\n",
    "\n",
    "我们想要通过卷积神经网络来对图像进行分类，因此需要使用卷积层来提取图像的特征。\n",
    "\n",
    "假设我们使用一个3x3的卷积核，并且设置输出通道数为16。那么，我们的卷积层的输入大小应该是28x28x3，输出大小应该是28x28x16。这意味着我们的网络将学习16个不同的特征，每个特征都可以从输入图像中提取出不同的信息。\n",
    "\n",
    "为什么选择3x3的卷积核？\n",
    "\n",
    "为什么通道数是16？"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "fdab5bb7",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import numpy as np\n",
    "from PIL import Image"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "85e7855c",
   "metadata": {},
   "source": [
    "其中，`in_channels`表示输入通道数，即输入图像的通道数；`out_channels`表示输出通道数，即卷积层的输出通道数；`kernel_size`表示卷积核的大小；`stride`表示卷积核滑动的步长；`padding`表示卷积核滑动前需要对输入数据进行填充的像素数。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23b1beb2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 定义卷积层\n",
    "conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce617fdf",
   "metadata": {},
   "source": [
    "不同的图像种类具有不同的通道数，通道数即图像的结构维度之上的意义维度。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c9ed6b86",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "灰度图像形状: (100, 100)\n"
     ]
    }
   ],
   "source": [
    "# 创建一个灰度图像（单通道） y隐式的通道数为1。访问的索引为0~99.\n",
    "gray_img = np.zeros((100, 100), dtype=np.uint8)         # 100x100像素\n",
    "print(f\"灰度图像形状: {gray_img.shape}\")                 # 输出: (100, 100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "63f67c11",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "坐标 (20, 30) 的灰度值: 0\n"
     ]
    }
   ],
   "source": [
    "# 查看一个灰度的大小\n",
    "# 访问第 20 行、第 30 列的像素灰度值（索引从 0 开始）\n",
    "pixel_value = gray_img[20, 30]                  # 注意顺序：先 row（行），后 column（列）\n",
    "print(f\"坐标 (20, 30) 的灰度值: {pixel_value}\")  # 输出: 0（初始值为全黑）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "48b2a612",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "RGB图像形状: (100, 100, 3)\n"
     ]
    }
   ],
   "source": [
    "# 创建一个RGB图像（三通道）\n",
    "rgb_img = np.zeros((100, 100, 3), dtype=np.uint8)  # 100x100x3\n",
    "print(f\"RGB图像形状: {rgb_img.shape}\")              # 输出: (100, 100, 3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "2158d0ba",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "像素(20, 30)的RGB值: [0 0 0]\n"
     ]
    }
   ],
   "source": [
    "# 访问第20行、第30列的像素的RGB值（返回长度为3的数组）\n",
    "pixel_rgb = rgb_img[20, 30]  # 格式: [R, G, B]\n",
    "print(f\"像素(20, 30)的RGB值: {pixel_rgb}\")  # 输出: [0 0 0]（初始值为全黑）"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f28ae000",
   "metadata": {},
   "source": [
    "我们简单的对RGB图像进行卷积。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef48a9a2",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 使用PyTorch的卷积层处理图像\n",
    "conv = torch.nn.Conv2d(\n",
    "    in_channels=3,      # 输入通道数（RGB为3）\n",
    "    out_channels=16,    # 输出通道数（16个卷积核）\n",
    "    kernel_size=3\n",
    ")\n",
    "\n",
    "\"\"\" \n",
    "    一个思考问题：如果卷积核的大小是依赖具体的数据场景和目的考量制约的，\n",
    "    输出通道数是我们整体结构层次之间的设计，那么输入通道数应该如何设计呢？\n",
    "    三个因素相互影响，但是它们怎么影响输出的结果形状的呢？\n",
    "    \n",
    "    首先，在现有的卷积层和数据形状下，我们可以提前明确的尺寸结果如下：\n",
    "        输出高度 = (输入高度 - 核大小) / 步长 + 1 = (100 - 3) / 1 + 1 = 98\n",
    "        输出宽度同理为98\n",
    "\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "88a5b5ac",
   "metadata": {},
   "source": [
    "一个错误的例子：这里的错误是因为我们首先要明确Numpy虽然从原理上是PyTorch都是张量库，但是它们并非结构一致的数据结构。或者说，我们认为类型并不匹配，尽管我们的数学角度认为它们是意义相同的。\n",
    "``` python \n",
    "output = conv(rgb_img)\n",
    "print(f\"卷积输出形状: {output.shape}\")  # 输出: [1, 16, 22, 22]\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "60a0081d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "卷积输出形状: torch.Size([1, 16, 98, 98])\n"
     ]
    }
   ],
   "source": [
    "\"\"\" \n",
    "    我们手动的逐步转化这个Image到合适类型和形状。\n",
    "\"\"\"\n",
    "# 1. 将numpy数组转换为PyTorch张量，满足类型需要。\n",
    "tensor_img = torch.from_numpy(rgb_img)\n",
    "\n",
    "# 2. 调整维度顺序：[H, W, C] → [C, H, W]\n",
    "tensor_img = tensor_img.permute(2, 0, 1)  # 交换维度，2→0, 0→1, 1→2\n",
    "\n",
    "# 3. 添加批次维度：[C, H, W] → [1, C, H, W]\n",
    "tensor_img = tensor_img.unsqueeze(0)  # 在第0维添加一个维度\n",
    "\n",
    "# 4. 转换数据类型：uint8 → float32，并归一化到[0,1]\n",
    "tensor_img = tensor_img.float() / 255.0\n",
    "\n",
    "# 5. 现在可以正确通过卷积层\n",
    "output = conv(tensor_img)\n",
    "print(f\"卷积输出形状: {output.shape}\")  # 应为 [1, 16, 98, 98]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "32fc203d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "卷积输出形状: torch.Size([1, 16, 98, 98])\n"
     ]
    }
   ],
   "source": [
    "\"\"\" \n",
    "    接下来我们使用更加简单的方式转化这个Numpy的Image到Tensor的形式。\n",
    "\"\"\"\n",
    "tensor_img = torch.from_numpy(rgb_img).permute(2, 0, 1).unsqueeze(0).float() / 255.0\n",
    "output = conv(tensor_img)\n",
    "print(f\"卷积输出形状: {output.shape}\")  # 输出: [1, 16, 22, 22]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "e2c07b4a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "卷积输出形状: torch.Size([1, 16, 98, 98])\n"
     ]
    }
   ],
   "source": [
    "\"\"\" \n",
    "    最后我们简单的重复创造一个符合要求的张量，但是使用直接使用Torch的方法。\n",
    "\"\"\"\n",
    "# 输入张量形状：[批次大小, 通道数, 高度, 宽度]\n",
    "input_tensor = torch.randn(1, 3, 100, 100)  # 1张100x100的RGB图像\n",
    "output = conv(input_tensor)\n",
    "print(f\"卷积输出形状: {output.shape}\")  # 输出: [1, 16, 98, 98]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd79d166",
   "metadata": {},
   "source": [
    "回忆：为什么卷积结果是这样的？为什么索引范围变化了？为什么通道数量变化了？我们什么时候需要这样的变化？什么时候需要保持原有尺寸？\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "1fae0b5d",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\" \n",
    "    我们尝试保持尺寸不变的卷积方法。\n",
    "    我们可以提前验证这个配置下的结果：\n",
    "        有效输入高度 = 原始高度 + 2×padding = 100 + 2×1 = 102\n",
    "        输出高度 = (有效输入高度 - 核大小) / 步长 + 1\n",
    "            = (102 - 3) / 1 + 1\n",
    "            = 99 + 1\n",
    "            = 100\n",
    "\"\"\"\n",
    "conv_keep_shape = torch.nn.Conv2d(\n",
    "    in_channels=3,\n",
    "    out_channels=16,\n",
    "    kernel_size=3,\n",
    "    padding=1  # 添加1像素的填充\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "16f08a6a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "卷积输出形状: torch.Size([1, 16, 100, 100])\n"
     ]
    }
   ],
   "source": [
    "input_tensor = torch.randn(1, 3, 100, 100)  # 1张100x100的RGB图像\n",
    "output = conv_keep_shape(input_tensor)\n",
    "print(f\"卷积输出形状: {output.shape}\")  # 输出: [1, 16, 98, 98]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "675e9cff",
   "metadata": {},
   "source": [
    "在什么情况下我们需要保持卷积后的尺寸不变，什么情况下我们变化？\n",
    "\n",
    "保持尺寸不变的情况\n",
    "1. 在图像分割任务的模型U-Net中，图像首先进入Encoder结构，该结构输出的特征图其目的在于下采样获取图像的特征，为了后续的解码器可以上采样解析，所以特征图的尺寸保持不变。并且这从语义上利于跳跃连接（Skip Connection）保持传递细节信息不变化。\n",
    "\n",
    "2. 网络深度增加的时候会导致分辨率下降，也就是说特征图逐渐的越来越小，这就失去了一定的细节。通过保持部分的层次尺寸不变，从而保留了信息。\n",
    "    1. ResNet中的残差块\n",
    "\n",
    "\n",
    "改变尺寸的情况\n",
    "1. 刻意的下采样，这降低了特征图的尺寸，从而降低了计算量。这种下采样让后续的网络可以集中注意力到全局的角度，从而可以识别对象的类别而非单纯的图像的边缘细节。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19e75d3e",
   "metadata": {},
   "source": [
    "如何保持不变？应该考虑的因素是什么？\n",
    "\n",
    "$$ \\text{输出尺寸} = \\left\\lfloor \\frac{\\text{输入尺寸} + 2 \\times \\text{填充} - \\text{卷积核尺寸}}{\\text{步长}} \\right\\rfloor + 1 $$\n",
    "\n",
    "首先我们假定步长不变的情况，（实际上，步长是影响特征图尺寸的关键因素。）\n",
    "\n",
    "$$ \\text{填充} = \\frac{\\text{卷积核尺寸} - 1}{2} $$\n",
    "\n",
    "通常，我们采用的卷积核似乎大小是3或者5，这意味着根据上面的式子我们的填充为1或者2。\n",
    "\n",
    "假定步长变化的情况，也就是步长大于1，这会要求对输入的数据使用：空洞卷积（Atrous Convolution），膨胀处理（Dilation）。\n",
    "\n",
    "为什么我们需要考虑卷积层的角度保持不变或者减小呢？因为在感受野这个话题上，我们有不同的方案去改变特征图的大小，而池化层就是一个办法。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "my_env",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
